Completed
Pull Request — master (#98)
by Ruben de
49s
created

Wallet.doDiscovery   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
c 0
b 0
f 0
nc 2
dl 0
loc 20
rs 9.4285
nop 2
1
var _ = require('lodash');
2
var assert = require('assert');
3
var q = require('q');
4
var async = require('async');
5
var bitcoin = require('bitcoinjs-lib');
6
var bitcoinMessage = require('bitcoinjs-message');
7
var blocktrail = require('./blocktrail');
8
var CryptoJS = require('crypto-js');
9
var Encryption = require('./encryption');
10
var EncryptionMnemonic = require('./encryption_mnemonic');
11
var SizeEstimation = require('./size_estimation');
12
var bip39 = require('bip39');
13
14
var SignMode = {
15
    SIGN: "sign",
16
    DONT_SIGN: "dont_sign"
17
};
18
19
/**
20
 *
21
 * @param sdk                   APIClient       SDK instance used to do requests
22
 * @param identifier            string          identifier of the wallet
23
 * @param walletVersion         string
24
 * @param primaryMnemonic       string          primary mnemonic
25
 * @param encryptedPrimarySeed
26
 * @param encryptedSecret
27
 * @param primaryPublicKeys     string          primary mnemonic
28
 * @param backupPublicKey       string          BIP32 master pubKey M/
29
 * @param blocktrailPublicKeys  array           list of blocktrail pubKeys indexed by keyIndex
30
 * @param keyIndex              int             key index to use
31
 * @param segwit                int             segwit toggle from server
32
 * @param testnet               bool            testnet
33
 * @param checksum              string
34
 * @param upgradeToKeyIndex     int
35
 * @param useNewCashAddr        bool            flag to opt in to bitcoin cash cashaddr's
36
 * @param bypassNewAddressCheck bool            flag to indicate if wallet should/shouldn't derive new address locally to verify api
37
 * @constructor
38
 * @internal
39
 */
40
var Wallet = function(
41
    sdk,
42
    identifier,
43
    walletVersion,
44
    primaryMnemonic,
45
    encryptedPrimarySeed,
46
    encryptedSecret,
47
    primaryPublicKeys,
48
    backupPublicKey,
49
    blocktrailPublicKeys,
50
    keyIndex,
51
    segwit,
52
    testnet,
53
    checksum,
54
    upgradeToKeyIndex,
55
    useNewCashAddr,
56
    bypassNewAddressCheck
57
) {
58
    /* jshint -W071 */
59
    var self = this;
60
61
    self.sdk = sdk;
62
    self.identifier = identifier;
63
    self.walletVersion = walletVersion;
64
    self.locked = true;
65
    self.bypassNewAddressCheck = !!bypassNewAddressCheck;
66
    self.bitcoinCash = self.sdk.bitcoinCash;
67
    self.segwit = !!segwit;
68
    self.useNewCashAddr = !!useNewCashAddr;
69
    assert(!self.segwit || !self.bitcoinCash);
70
71
    self.testnet = testnet;
72
    if (self.bitcoinCash) {
73
        if (self.testnet) {
74
            self.network = bitcoin.networks.bitcoincashtestnet;
75
        } else {
76
            self.network = bitcoin.networks.bitcoincash;
77
        }
78
    } else {
79
        if (self.testnet) {
80
            self.network = bitcoin.networks.testnet;
81
        } else {
82
            self.network = bitcoin.networks.bitcoin;
83
        }
84
    }
85
86
    assert(backupPublicKey instanceof bitcoin.HDNode);
87
    assert(_.every(primaryPublicKeys, function(primaryPublicKey) { return primaryPublicKey instanceof bitcoin.HDNode; }));
88
    assert(_.every(blocktrailPublicKeys, function(blocktrailPublicKey) { return blocktrailPublicKey instanceof bitcoin.HDNode; }));
89
90
    // v1
91
    self.primaryMnemonic = primaryMnemonic;
92
93
    // v2 & v3
94
    self.encryptedPrimarySeed = encryptedPrimarySeed;
95
    self.encryptedSecret = encryptedSecret;
96
97
    self.primaryPrivateKey = null;
98
    self.backupPrivateKey = null;
99
100
    self.backupPublicKey = backupPublicKey;
101
    self.blocktrailPublicKeys = blocktrailPublicKeys;
102
    self.primaryPublicKeys = primaryPublicKeys;
103
    self.keyIndex = keyIndex;
104
105
    if (!self.bitcoinCash) {
106
        if (self.segwit) {
107
            self.chain = Wallet.CHAIN_BTC_DEFAULT;
108
            self.changeChain = Wallet.CHAIN_BTC_SEGWIT;
109
        } else {
110
            self.chain = Wallet.CHAIN_BTC_DEFAULT;
111
            self.changeChain = Wallet.CHAIN_BTC_DEFAULT;
112
        }
113
    } else {
114
        self.chain = Wallet.CHAIN_BCC_DEFAULT;
115
        self.changeChain = Wallet.CHAIN_BCC_DEFAULT;
116
    }
117
118
    self.checksum = checksum;
119
    self.upgradeToKeyIndex = upgradeToKeyIndex;
120
121
    self.secret = null;
122
    self.seedHex = null;
123
};
124
125
Wallet.WALLET_VERSION_V1 = 'v1';
126
Wallet.WALLET_VERSION_V2 = 'v2';
127
Wallet.WALLET_VERSION_V3 = 'v3';
128
129
Wallet.WALLET_ENTROPY_BITS = 256;
130
131
Wallet.OP_RETURN = 'opreturn';
132
Wallet.DATA = Wallet.OP_RETURN; // alias
133
134
Wallet.PAY_PROGRESS_START = 0;
135
Wallet.PAY_PROGRESS_COIN_SELECTION = 10;
136
Wallet.PAY_PROGRESS_CHANGE_ADDRESS = 20;
137
Wallet.PAY_PROGRESS_SIGN = 30;
138
Wallet.PAY_PROGRESS_SEND = 40;
139
Wallet.PAY_PROGRESS_DONE = 100;
140
141
Wallet.CHAIN_BTC_DEFAULT = 0;
142
Wallet.CHAIN_BTC_SEGWIT = 2;
143
Wallet.CHAIN_BCC_DEFAULT = 1;
144
145
Wallet.FEE_STRATEGY_FORCE_FEE = blocktrail.FEE_STRATEGY_FORCE_FEE;
146
Wallet.FEE_STRATEGY_BASE_FEE = blocktrail.FEE_STRATEGY_BASE_FEE;
147
Wallet.FEE_STRATEGY_HIGH_PRIORITY = blocktrail.FEE_STRATEGY_HIGH_PRIORITY;
148
Wallet.FEE_STRATEGY_OPTIMAL = blocktrail.FEE_STRATEGY_OPTIMAL;
149
Wallet.FEE_STRATEGY_LOW_PRIORITY = blocktrail.FEE_STRATEGY_LOW_PRIORITY;
150
Wallet.FEE_STRATEGY_MIN_RELAY_FEE = blocktrail.FEE_STRATEGY_MIN_RELAY_FEE;
151
152
Wallet.prototype.isSegwit = function() {
153
    return !!this.segwit;
154
};
155
156
Wallet.prototype.unlock = function(options, cb) {
157
    var self = this;
158
159
    var deferred = q.defer();
160
    deferred.promise.nodeify(cb);
161
162
    // avoid modifying passed options
163
    options = _.merge({}, options);
164
165
    q.fcall(function() {
166
        switch (self.walletVersion) {
167
            case Wallet.WALLET_VERSION_V1:
0 ignored issues
show
Bug introduced by
The variable Wallet seems to be never declared. If this is a global, consider adding a /** global: Wallet */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
168
                return self.unlockV1(options);
169
170
            case Wallet.WALLET_VERSION_V2:
171
                return self.unlockV2(options);
172
173
            case Wallet.WALLET_VERSION_V3:
174
                return self.unlockV3(options);
175
176
            default:
177
                return q.reject(new blocktrail.WalletInitError("Invalid wallet version"));
178
        }
179
    }).then(
180
        function(primaryPrivateKey) {
181
            self.primaryPrivateKey = primaryPrivateKey;
182
183
            // create a checksum of our private key which we'll later use to verify we used the right password
184
            var checksum = self.primaryPrivateKey.getAddress();
185
186
            // check if we've used the right passphrase
187
            if (checksum !== self.checksum) {
188
                throw new blocktrail.WalletChecksumError("Generated checksum [" + checksum + "] does not match " +
189
                    "[" + self.checksum + "], most likely due to incorrect password");
190
            }
191
192
            self.locked = false;
193
194
            // if the response suggests we should upgrade to a different blocktrail cosigning key then we should
195
            if (typeof self.upgradeToKeyIndex !== "undefined" && self.upgradeToKeyIndex !== null) {
0 ignored issues
show
Complexity Best Practice introduced by
There is no return statement if typeof self.upgradeToKey...radeToKeyIndex !== null is false. Are you sure this is correct? If so, consider adding return; explicitly.

This check looks for functions where a return statement is found in some execution paths, but not in all.

Consider this little piece of code

function isBig(a) {
    if (a > 5000) {
        return "yes";
    }
}

console.log(isBig(5001)); //returns yes
console.log(isBig(42)); //returns undefined

The function isBig will only return a specific value when its parameter is bigger than 5000. In any other case, it will implicitly return undefined.

This behaviour may not be what you had intended. In any case, you can add a return undefined to the other execution path to make the return value explicit.

Loading history...
196
                return self.upgradeKeyIndex(self.upgradeToKeyIndex);
197
            }
198
        }
199
    ).then(
200
        function(r) {
201
            deferred.resolve(r);
202
        },
203
        function(e) {
204
            deferred.reject(e);
205
        }
206
    );
207
208
    return deferred.promise;
209
};
210
211
Wallet.prototype.unlockV1 = function(options) {
212
    var self = this;
213
214
    options.primaryMnemonic = typeof options.primaryMnemonic !== "undefined" ? options.primaryMnemonic : self.primaryMnemonic;
215
    options.secretMnemonic = typeof options.secretMnemonic !== "undefined" ? options.secretMnemonic : self.secretMnemonic;
216
217
    return self.sdk.resolvePrimaryPrivateKeyFromOptions(options)
218
        .then(function(options) {
219
            self.primarySeed = options.primarySeed;
220
221
            return options.primaryPrivateKey;
222
        });
223
};
224
225
Wallet.prototype.unlockV2 = function(options, cb) {
226
    var self = this;
227
228
    var deferred = q.defer();
229
    deferred.promise.nodeify(cb);
230
231
    deferred.resolve(q.fcall(function() {
232
        /* jshint -W071, -W074 */
233
        options.encryptedPrimarySeed = typeof options.encryptedPrimarySeed !== "undefined" ? options.encryptedPrimarySeed : self.encryptedPrimarySeed;
234
        options.encryptedSecret = typeof options.encryptedSecret !== "undefined" ? options.encryptedSecret : self.encryptedSecret;
235
236
        if (options.secret) {
237
            self.secret = options.secret;
238
        }
239
240
        if (options.primaryPrivateKey) {
241
            throw new blocktrail.WalletDecryptError("specifying primaryPrivateKey has been deprecated");
242
        }
243
244
        if (options.primarySeed) {
245
            self.primarySeed = options.primarySeed;
246
        } else if (options.secret) {
247
            try {
248
                self.primarySeed = new Buffer(
0 ignored issues
show
Bug introduced by
The variable Buffer seems to be never declared. If this is a global, consider adding a /** global: Buffer */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
249
                    CryptoJS.AES.decrypt(CryptoJS.format.OpenSSL.parse(options.encryptedPrimarySeed), self.secret).toString(CryptoJS.enc.Utf8), 'base64');
250
                if (!self.primarySeed.length) {
251
                    throw new Error();
252
                }
253
            } catch (e) {
254
                throw new blocktrail.WalletDecryptError("Failed to decrypt primarySeed");
255
            }
256
257
        } else {
258
            // avoid conflicting options
259
            if (options.passphrase && options.password) {
260
                throw new blocktrail.WalletCreateError("Can't specify passphrase and password");
261
            }
262
            // normalize passphrase/password
263
            options.passphrase = options.passphrase || options.password;
264
265
            try {
266
                self.secret = CryptoJS.AES.decrypt(CryptoJS.format.OpenSSL.parse(options.encryptedSecret), options.passphrase).toString(CryptoJS.enc.Utf8);
267
                if (!self.secret.length) {
268
                    throw new Error();
269
                }
270
            } catch (e) {
271
                throw new blocktrail.WalletDecryptError("Failed to decrypt secret");
272
            }
273
            try {
274
                self.primarySeed = new Buffer(
275
                    CryptoJS.AES.decrypt(CryptoJS.format.OpenSSL.parse(options.encryptedPrimarySeed), self.secret).toString(CryptoJS.enc.Utf8), 'base64');
276
                if (!self.primarySeed.length) {
277
                    throw new Error();
278
                }
279
            } catch (e) {
280
                throw new blocktrail.WalletDecryptError("Failed to decrypt primarySeed");
281
            }
282
        }
283
284
        return bitcoin.HDNode.fromSeedBuffer(self.primarySeed, self.network);
285
    }));
286
287
    return deferred.promise;
288
};
289
290
Wallet.prototype.unlockV3 = function(options, cb) {
291
    var self = this;
292
293
    var deferred = q.defer();
294
    deferred.promise.nodeify(cb);
295
296
    deferred.resolve(q.fcall(function() {
297
        return q.when()
298
            .then(function() {
299
                /* jshint -W071, -W074 */
300
                options.encryptedPrimarySeed = typeof options.encryptedPrimarySeed !== "undefined" ? options.encryptedPrimarySeed : self.encryptedPrimarySeed;
301
                options.encryptedSecret = typeof options.encryptedSecret !== "undefined" ? options.encryptedSecret : self.encryptedSecret;
302
303
                if (options.secret) {
304
                    self.secret = options.secret;
305
                }
306
307
                if (options.primaryPrivateKey) {
308
                    throw new blocktrail.WalletInitError("specifying primaryPrivateKey has been deprecated");
309
                }
310
311
                if (options.primarySeed) {
312
                    self.primarySeed = options.primarySeed;
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
313
                } else if (options.secret) {
314
                    return self.sdk.promisedDecrypt(new Buffer(options.encryptedPrimarySeed, 'base64'), self.secret)
0 ignored issues
show
Bug introduced by
The variable Buffer seems to be never declared. If this is a global, consider adding a /** global: Buffer */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
315
                        .then(function(primarySeed) {
316
                            self.primarySeed = primarySeed;
317
                        }, function() {
318
                            throw new blocktrail.WalletDecryptError("Failed to decrypt primarySeed");
319
                        });
320
                } else {
321
                    // avoid conflicting options
322
                    if (options.passphrase && options.password) {
323
                        throw new blocktrail.WalletCreateError("Can't specify passphrase and password");
324
                    }
325
                    // normalize passphrase/password
326
                    options.passphrase = options.passphrase || options.password;
327
                    delete options.password;
328
329
                    return self.sdk.promisedDecrypt(new Buffer(options.encryptedSecret, 'base64'), new Buffer(options.passphrase))
0 ignored issues
show
Bug introduced by
The variable Buffer seems to be never declared. If this is a global, consider adding a /** global: Buffer */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
330
                        .then(function(secret) {
331
                            self.secret = secret;
332
                        }, function() {
333
                            throw new blocktrail.WalletDecryptError("Failed to decrypt secret");
334
                        })
335
                        .then(function() {
336
                            return self.sdk.promisedDecrypt(new Buffer(options.encryptedPrimarySeed, 'base64'), self.secret)
0 ignored issues
show
Bug introduced by
The variable Buffer seems to be never declared. If this is a global, consider adding a /** global: Buffer */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
337
                                .then(function(primarySeed) {
338
                                    self.primarySeed = primarySeed;
339
                                }, function() {
340
                                    throw new blocktrail.WalletDecryptError("Failed to decrypt primarySeed");
341
                                });
342
                        });
343
                }
344
            })
345
            .then(function() {
346
                return bitcoin.HDNode.fromSeedBuffer(self.primarySeed, self.network);
347
            })
348
        ;
349
    }));
350
351
    return deferred.promise;
352
};
353
354
Wallet.prototype.lock = function() {
355
    var self = this;
356
357
    self.secret = null;
358
    self.primarySeed = null;
359
    self.primaryPrivateKey = null;
360
    self.backupPrivateKey = null;
361
362
    self.locked = true;
363
};
364
365
/**
366
 * upgrade wallet to V3 encryption scheme
367
 *
368
 * @param passphrase is required again to reencrypt the data, important that it's the correct password!!!
369
 * @param cb
370
 * @returns {promise}
371
 */
372
Wallet.prototype.upgradeToV3 = function(passphrase, cb) {
373
    var self = this;
374
375
    var deferred = q.defer();
376
    deferred.promise.nodeify(cb);
377
378
    q.when(true)
379
        .then(function() {
380
            if (self.locked) {
381
                throw new blocktrail.WalletLockedError("Wallet needs to be unlocked to upgrade");
382
            }
383
384
            if (self.walletVersion === Wallet.WALLET_VERSION_V3) {
385
                throw new blocktrail.WalletUpgradeError("Wallet is already V3");
386
            } else if (self.walletVersion === Wallet.WALLET_VERSION_V2) {
387
                return self._upgradeV2ToV3(passphrase, deferred.notify.bind(deferred));
388
            } else if (self.walletVersion === Wallet.WALLET_VERSION_V1) {
0 ignored issues
show
Complexity Best Practice introduced by
There is no return statement if self.walletVersion === Wallet.WALLET_VERSION_V1 is false. Are you sure this is correct? If so, consider adding return; explicitly.

This check looks for functions where a return statement is found in some execution paths, but not in all.

Consider this little piece of code

function isBig(a) {
    if (a > 5000) {
        return "yes";
    }
}

console.log(isBig(5001)); //returns yes
console.log(isBig(42)); //returns undefined

The function isBig will only return a specific value when its parameter is bigger than 5000. In any other case, it will implicitly return undefined.

This behaviour may not be what you had intended. In any case, you can add a return undefined to the other execution path to make the return value explicit.

Loading history...
389
                return self._upgradeV1ToV3(passphrase, deferred.notify.bind(deferred));
390
            }
391
        })
392
        .then(function(r) { deferred.resolve(r); }, function(e) { deferred.reject(e); });
393
394
    return deferred.promise;
395
};
396
397
Wallet.prototype._upgradeV2ToV3 = function(passphrase, notify) {
398
    var self = this;
399
400
    return q.when(true)
401
        .then(function() {
402
            var options = {
403
                storeDataOnServer: true,
404
                passphrase: passphrase,
405
                primarySeed: self.primarySeed,
406
                recoverySecret: false // don't create new recovery secret, V2 already has ones
407
            };
408
409
            return self.sdk.produceEncryptedDataV3(options, notify || function noop() {})
410
                .then(function(options) {
411
                    return self.sdk.updateWallet(self.identifier, {
412
                        encrypted_primary_seed: options.encryptedPrimarySeed.toString('base64'),
413
                        encrypted_secret: options.encryptedSecret.toString('base64'),
414
                        wallet_version: Wallet.WALLET_VERSION_V3
415
                    }).then(function() {
416 View Code Duplication
                        self.secret = options.secret;
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
417
                        self.encryptedPrimarySeed = options.encryptedPrimarySeed;
418
                        self.encryptedSecret = options.encryptedSecret;
419
                        self.walletVersion = Wallet.WALLET_VERSION_V3;
420
421
                        return self;
422
                    });
423
                });
424
        });
425
426
};
427
428
Wallet.prototype._upgradeV1ToV3 = function(passphrase, notify) {
429
    var self = this;
430
431
    return q.when(true)
432
        .then(function() {
433
            var options = {
434
                storeDataOnServer: true,
435
                passphrase: passphrase,
436
                primarySeed: self.primarySeed
437
            };
438
439
            return self.sdk.produceEncryptedDataV3(options, notify || function noop() {})
440
                .then(function(options) {
441
                    // store recoveryEncryptedSecret for printing on backup sheet
442
                    self.recoveryEncryptedSecret = options.recoveryEncryptedSecret;
443
444
                    return self.sdk.updateWallet(self.identifier, {
445
                        primary_mnemonic: '',
446
                        encrypted_primary_seed: options.encryptedPrimarySeed.toString('base64'),
447
                        encrypted_secret: options.encryptedSecret.toString('base64'),
448
                        recovery_secret: options.recoverySecret.toString('hex'),
449
                        wallet_version: Wallet.WALLET_VERSION_V3
450
                    }).then(function() {
451
                        self.secret = options.secret;
452
                        self.encryptedPrimarySeed = options.encryptedPrimarySeed;
453
                        self.encryptedSecret = options.encryptedSecret;
454
                        self.walletVersion = Wallet.WALLET_VERSION_V3;
455
456
                        return self;
457
                    });
458
                });
459
        });
460
};
461
462
Wallet.prototype.doPasswordChange = function(newPassword) {
463
    var self = this;
464
465
    return q.when(null)
466
        .then(function() {
467
468
            if (self.walletVersion === Wallet.WALLET_VERSION_V1) {
469
                throw new blocktrail.WalletLockedError("Wallet version does not support password change!");
470
            }
471
472
            if (self.locked) {
473
                throw new blocktrail.WalletLockedError("Wallet needs to be unlocked to change password");
474
            }
475
476
            if (!self.secret) {
477
                throw new blocktrail.WalletLockedError("No secret");
478
            }
479
480
            var newEncryptedSecret;
481
            var newEncrypedWalletSecretMnemonic;
482
            if (self.walletVersion === Wallet.WALLET_VERSION_V2) {
483
                newEncryptedSecret = CryptoJS.AES.encrypt(self.secret, newPassword).toString(CryptoJS.format.OpenSSL);
484
                newEncrypedWalletSecretMnemonic = bip39.entropyToMnemonic(blocktrail.convert(newEncryptedSecret, 'base64', 'hex'));
485
486
            } else {
487
                if (typeof newPassword === "string") {
488
                    newPassword = new Buffer(newPassword);
0 ignored issues
show
Bug introduced by
The variable Buffer seems to be never declared. If this is a global, consider adding a /** global: Buffer */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
489
                } else {
490
                    if (!(newPassword instanceof Buffer)) {
491
                        throw new Error('New password must be provided as a string or a Buffer');
492
                    }
493
                }
494
495
                newEncryptedSecret = Encryption.encrypt(self.secret, newPassword);
496
                newEncrypedWalletSecretMnemonic = EncryptionMnemonic.encode(newEncryptedSecret);
497
498
                // It's a buffer, so convert it back to base64
499
                newEncryptedSecret = newEncryptedSecret.toString('base64');
500
            }
501
502
            return [newEncryptedSecret, newEncrypedWalletSecretMnemonic];
503
        });
504
};
505
506
Wallet.prototype.passwordChange = function(newPassword, cb) {
507
    var self = this;
508
509
    var deferred = q.defer();
510
    deferred.promise.nodeify(cb);
511
512
    q.fcall(function() {
513
        return self.doPasswordChange(newPassword)
514
            .then(function(r) {
515
                var newEncryptedSecret = r[0];
516
                var newEncrypedWalletSecretMnemonic = r[1];
517
518
                return self.sdk.updateWallet(self.identifier, {encrypted_secret: newEncryptedSecret}).then(function() {
519
                    self.encryptedSecret = newEncryptedSecret;
520
521
                    // backupInfo
522
                    return {
523
                        encryptedSecret: newEncrypedWalletSecretMnemonic
524
                    };
525
                });
526
            })
527
            .then(
528
                function(r) {
529
                    deferred.resolve(r);
530
                },
531
                function(e) {
532
                    deferred.reject(e);
533
                }
534
            );
535
    });
536
537
    return deferred.promise;
538
};
539
540
/**
541
 * get address for specified path
542
 *
543
 * @param path
544
 * @returns string
545
 */
546
Wallet.prototype.getAddressByPath = function(path) {
547
    return this.getWalletScriptByPath(path).address;
548
};
549
550
/**
551
 * get redeemscript for specified path
552
 *
553
 * @param path
554
 * @returns {Buffer}
555
 */
556
Wallet.prototype.getRedeemScriptByPath = function(path) {
557
    return this.getWalletScriptByPath(path).redeemScript;
558
};
559
560
/**
561
 * Generate scripts, and address.
562
 * @param path
563
 * @returns {{witnessScript: *, redeemScript: *, scriptPubKey, address: *}}
564
 */
565
Wallet.prototype.getWalletScriptByPath = function(path) {
566
    var self = this;
567
568
    // get derived primary key
569
    var derivedPrimaryPublicKey = self.getPrimaryPublicKey(path);
570
    // get derived blocktrail key
571
    var derivedBlocktrailPublicKey = self.getBlocktrailPublicKey(path);
572
    // derive the backup key
573
    var derivedBackupPublicKey = Wallet.deriveByPath(self.backupPublicKey, path.replace("'", ""), "M");
574
575
    // sort the pubkeys
576
    var pubKeys = Wallet.sortMultiSigKeys([
577
        derivedPrimaryPublicKey.keyPair.getPublicKeyBuffer(),
578
        derivedBackupPublicKey.keyPair.getPublicKeyBuffer(),
579
        derivedBlocktrailPublicKey.keyPair.getPublicKeyBuffer()
580
    ]);
581
582
    var multisig = bitcoin.script.multisig.output.encode(2, pubKeys);
583
    var scriptType = parseInt(path.split("/")[2]);
584
585
    var ws, rs;
586
    if (this.network !== "bitcoincash" && scriptType === Wallet.CHAIN_BTC_SEGWIT) {
587
        ws = multisig;
588
        rs = bitcoin.script.witnessScriptHash.output.encode(bitcoin.crypto.sha256(ws));
589
    } else {
590
        ws = null;
591
        rs = multisig;
592
    }
593
594
    var spk = bitcoin.script.scriptHash.output.encode(bitcoin.crypto.hash160(rs));
595
    var addr = bitcoin.address.fromOutputScript(spk, this.network, self.useNewCashAddr);
596
597
    return {
598
        witnessScript: ws,
599
        redeemScript: rs,
600
        scriptPubKey: spk,
601
        address: addr
602
    };
603
};
604
605
/**
606
 * get primary public key by path
607
 *  first level of the path is used as keyIndex to find the correct key in the dict
608
 *
609
 * @param path  string
610
 * @returns {bitcoin.HDNode}
611
 */
612
Wallet.prototype.getPrimaryPublicKey = function(path) {
613
    var self = this;
614
615
    path = path.replace("m", "M");
616
617
    var keyIndex = path.split("/")[1].replace("'", "");
618
619
    if (!self.primaryPublicKeys[keyIndex]) {
620
        if (self.primaryPrivateKey) {
621
            self.primaryPublicKeys[keyIndex] = Wallet.deriveByPath(self.primaryPrivateKey, "M/" + keyIndex + "'", "m");
622
        } else {
623
            throw new blocktrail.KeyPathError("Wallet.getPrimaryPublicKey keyIndex (" + keyIndex + ") is unknown to us");
624
        }
625
    }
626
627
    var primaryPublicKey = self.primaryPublicKeys[keyIndex];
628
    return Wallet.deriveByPath(primaryPublicKey, path, "M/" + keyIndex + "'");
629
};
630
631
/**
632
 * get blocktrail public key by path
633
 *  first level of the path is used as keyIndex to find the correct key in the dict
634
 *
635
 * @param path  string
636
 * @returns {bitcoin.HDNode}
637
 */
638
Wallet.prototype.getBlocktrailPublicKey = function(path) {
639
    var self = this;
640
641
    path = path.replace("m", "M");
642
643
    var keyIndex = path.split("/")[1].replace("'", "");
644
645
    if (!self.blocktrailPublicKeys[keyIndex]) {
646
        throw new blocktrail.KeyPathError("Wallet.getBlocktrailPublicKey keyIndex (" + keyIndex + ") is unknown to us");
647
    }
648
649
    var blocktrailPublicKey = self.blocktrailPublicKeys[keyIndex];
650
651
    return Wallet.deriveByPath(blocktrailPublicKey, path, "M/" + keyIndex + "'");
652
};
653
654
/**
655
 * upgrade wallet to different blocktrail cosign key
656
 *
657
 * @param keyIndex  int
658
 * @param [cb]      function
659
 * @returns {q.Promise}
660
 */
661
Wallet.prototype.upgradeKeyIndex = function(keyIndex, cb) {
662
    var self = this;
663
664
    var deferred = q.defer();
665
    deferred.promise.nodeify(cb);
666
667
    if (self.locked) {
668
        deferred.reject(new blocktrail.WalletLockedError("Wallet needs to be unlocked to upgrade key index"));
669
        return deferred.promise;
670
    }
671
672
    var primaryPublicKey = self.primaryPrivateKey.deriveHardened(keyIndex).neutered();
673
674
    deferred.resolve(
675
        self.sdk.upgradeKeyIndex(self.identifier, keyIndex, [primaryPublicKey.toBase58(), "M/" + keyIndex + "'"])
676
            .then(function(result) {
677
                self.keyIndex = keyIndex;
678
                _.forEach(result.blocktrail_public_keys, function(publicKey, keyIndex) {
679
                    self.blocktrailPublicKeys[keyIndex] = bitcoin.HDNode.fromBase58(publicKey[0], self.network);
680
                });
681
682
                self.primaryPublicKeys[keyIndex] = primaryPublicKey;
683
684
                return true;
685
            })
686
    );
687
688
    return deferred.promise;
689
};
690
691
/**
692
 * generate a new derived private key and return the new address for it
693
 *
694
 * @param [chainIdx] int
695
 * @param [cb]  function        callback(err, address)
696
 * @returns {q.Promise}
697
 */
698
Wallet.prototype.getNewAddress = function(chainIdx, cb) {
699
    var self = this;
700
701
    // chainIdx is optional
702
    if (typeof chainIdx === "function") {
703
        cb = chainIdx;
704
        chainIdx = null;
705
    }
706
707
    var deferred = q.defer();
708
    deferred.promise.spreadNodeify(cb);
709
710
    // Only enter if it's not an integer
711
    if (chainIdx !== parseInt(chainIdx, 10)) {
712
        // deal with undefined or null, assume defaults
713
        if (typeof chainIdx === "undefined" || chainIdx === null) {
714
            chainIdx = self.chain;
715
        } else {
716
            // was a variable but not integer
717
            deferred.reject(new Error("Invalid chain index"));
718
            return deferred.promise;
719
        }
720
    }
721
722
    deferred.resolve(
723
        self.sdk.getNewDerivation(self.identifier, "M/" + self.keyIndex + "'/" + chainIdx)
724
            .then(function(newDerivation) {
725
                var path = newDerivation.path;
726
                var addressFromServer = newDerivation.address;
727
                var decodedFromServer;
728
729
                try {
730
                    // Decode the address the serer gave us
731
                    decodedFromServer = self.decodeAddress(addressFromServer);
732
                    if ("cashAddrPrefix" in self.network && self.useNewCashAddr && decodedFromServer.type === "base58") {
733
                        self.bypassNewAddressCheck = false;
734
                    }
735
                } catch (e) {
736
                    throw new blocktrail.WalletAddressError("Failed to decode address [" + newDerivation.address + "]");
737
                }
738
739
                if (!self.bypassNewAddressCheck) {
740
                    // We need to reproduce this address with the same path,
741
                    // but the server (for BCH cashaddrs) uses base58?
742
                    var verifyAddress = self.getAddressByPath(newDerivation.path);
743
744
                    // If this occasion arises:
745
                    if ("cashAddrPrefix" in self.network && self.useNewCashAddr && decodedFromServer.type === "base58") {
746
                        // Decode our the address we produced for the path
747
                        var decodeOurs;
748
                        try {
749
                            decodeOurs = self.decodeAddress(verifyAddress);
750
                        } catch (e) {
751
                            throw new blocktrail.WalletAddressError("Error while verifying address from server [" + e.message + "]");
752
                        }
753
754
                        // Peek beyond the encoding - the hashes must match at least
755
                        if (decodeOurs.decoded.hash.toString('hex') !== decodedFromServer.decoded.hash.toString('hex')) {
756
                            throw new blocktrail.WalletAddressError("Failed to verify legacy address [hash mismatch]");
757
                        }
758
759
                        var matchedP2PKH = decodeOurs.decoded.version === bitcoin.script.types.P2PKH &&
760
                            decodedFromServer.decoded.version === self.network.pubKeyHash;
761
                        var matchedP2SH = decodeOurs.decoded.version === bitcoin.script.types.P2SH &&
762
                            decodedFromServer.decoded.version === self.network.scriptHash;
763
764
                        if (!(matchedP2PKH || matchedP2SH)) {
765
                            throw new blocktrail.WalletAddressError("Failed to verify legacy address [prefix mismatch]");
766
                        }
767
768
                        // We are satisfied that the address is for the same
769
                        // destination, so substitute addressFromServer with our
770
                        // 'reencoded' form.
771
                        addressFromServer = decodeOurs.address;
772
                    }
773
774
                    // debug check
775
                    if (verifyAddress !== addressFromServer) {
776
                        throw new blocktrail.WalletAddressError("Failed to verify address [" + newDerivation.address + "] !== [" + addressFromServer + "]");
777
                    }
778
                }
779
780
                return [addressFromServer, path];
781
            })
782
    );
783
784
    return deferred.promise;
785
};
786
787
/**
788
 * get the balance for the wallet
789 View Code Duplication
 *
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
790
 * @param [cb]  function        callback(err, confirmed, unconfirmed)
791
 * @returns {q.Promise}
792
 */
793
Wallet.prototype.getBalance = function(cb) {
794
    var self = this;
795
796
    var deferred = q.defer();
797
    deferred.promise.spreadNodeify(cb);
798
799
    deferred.resolve(
800
        self.sdk.getWalletBalance(self.identifier)
801
            .then(function(result) {
802
                return [result.confirmed, result.unconfirmed];
803
            })
804
    );
805
806
    return deferred.promise;
807
};
808
809
/**
810
 * get the balance for the wallet
811
 *
812
 * @param [cb]  function        callback(err, confirmed, unconfirmed)
813
 * @returns {q.Promise}
814
 */
815
Wallet.prototype.getInfo = function(cb) {
816
    var self = this;
817
818
    var deferred = q.defer();
819
    deferred.promise.spreadNodeify(cb);
820
821
    deferred.resolve(
822
        self.sdk.getWalletBalance(self.identifier)
823
    );
824
825
    return deferred.promise;
826
};
827
828
/**
829
 *
830
 * @param [force]   bool            ignore warnings (such as non-zero balance)
831
 * @param [cb]      function        callback(err, success)
832
 * @returns {q.Promise}
833
 */
834
Wallet.prototype.deleteWallet = function(force, cb) {
835
    var self = this;
836
837
    if (typeof force === "function") {
838
        cb = force;
839
        force = false;
840
    }
841
842
    var deferred = q.defer();
843
    deferred.promise.nodeify(cb);
844
845
    if (self.locked) {
846
        deferred.reject(new blocktrail.WalletDeleteError("Wallet needs to be unlocked to delete wallet"));
847
        return deferred.promise;
848
    }
849
850
    var checksum = self.primaryPrivateKey.getAddress();
851
    var privBuf = self.primaryPrivateKey.keyPair.d.toBuffer(32);
852
    var signature = bitcoinMessage.sign(checksum, self.network.messagePrefix, privBuf, true).toString('base64');
853
854
    deferred.resolve(
855
        self.sdk.deleteWallet(self.identifier, checksum, signature, force)
856
            .then(function(result) {
857
                return result.deleted;
858
            })
859
    );
860
861
    return deferred.promise;
862
};
863
864
/**
865
 * create, sign and send a transaction
866
 *
867 View Code Duplication
 * @param pay                   array       {'address': (int)value}     coins to send
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
868
 * @param [changeAddress]       bool        change address to use (auto generated if NULL)
869
 * @param [allowZeroConf]       bool        allow zero confirmation unspent outputs to be used in coin selection
870
 * @param [randomizeChangeIdx]  bool        randomize the index of the change output (default TRUE, only disable if you have a good reason to)
871
 * @param [feeStrategy]         string      defaults to Wallet.FEE_STRATEGY_OPTIMAL
872
 * @param [twoFactorToken]      string      2FA token
873
 * @param options
874
 * @param [cb]                  function    callback(err, txHash)
875
 * @returns {q.Promise}
876
 */
877
Wallet.prototype.pay = function(pay, changeAddress, allowZeroConf, randomizeChangeIdx, feeStrategy, twoFactorToken, options, cb) {
878
879
    /* jshint -W071 */
880
    var self = this;
881
882
    if (typeof changeAddress === "function") {
883
        cb = changeAddress;
884
        changeAddress = null;
885
    } else if (typeof allowZeroConf === "function") {
886
        cb = allowZeroConf;
887
        allowZeroConf = false;
888
    } else if (typeof randomizeChangeIdx === "function") {
889
        cb = randomizeChangeIdx;
890
        randomizeChangeIdx = true;
891
    } else if (typeof feeStrategy === "function") {
892
        cb = feeStrategy;
893
        feeStrategy = null;
894
    } else if (typeof twoFactorToken === "function") {
895
        cb = twoFactorToken;
896
        twoFactorToken = null;
897
    } else if (typeof options === "function") {
898
        cb = options;
899
        options = {};
900
    }
901
902
    randomizeChangeIdx = typeof randomizeChangeIdx !== "undefined" ? randomizeChangeIdx : true;
903
    feeStrategy = feeStrategy || Wallet.FEE_STRATEGY_OPTIMAL;
904
    options = options || {};
905
    var checkFee = typeof options.checkFee !== "undefined" ? options.checkFee : true;
906
907
    var deferred = q.defer();
908
    deferred.promise.nodeify(cb);
909
910
    if (self.locked) {
911
        deferred.reject(new blocktrail.WalletLockedError("Wallet needs to be unlocked to send coins"));
912
        return deferred.promise;
913
    }
914
915
    q.nextTick(function() {
916
        deferred.notify(Wallet.PAY_PROGRESS_START);
917
        self.buildTransaction(pay, changeAddress, allowZeroConf, randomizeChangeIdx, feeStrategy, options)
918
            .then(
919
            function(r) { return r; },
920
            function(e) { deferred.reject(e); },
921
            function(progress) {
922
                deferred.notify(progress);
923
            }
924
        )
925
            .spread(
926
            function(tx, utxos) {
927
928
                deferred.notify(Wallet.PAY_PROGRESS_SEND);
929
930
                var data = {
931
                    signed_transaction: tx.toHex(),
932
                    base_transaction: tx.__toBuffer(null, null, false).toString('hex')
933
                };
934
935
                return self.sendTransaction(data, utxos.map(function(utxo) { return utxo['path']; }), checkFee, twoFactorToken, options.prioboost)
936
                    .then(function(result) {
937
                        deferred.notify(Wallet.PAY_PROGRESS_DONE);
938
939
                        if (!result || !result['complete'] || result['complete'] === 'false') {
940
                            deferred.reject(new blocktrail.TransactionSignError("Failed to completely sign transaction"));
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
941
                        } else {
942
                            return result['txid'];
943
                        }
944
                    });
945
            },
946
            function(e) {
947
                throw e;
948
            }
949
        )
950
            .then(
951
            function(r) { deferred.resolve(r); },
952
            function(e) { deferred.reject(e); }
953
        )
954
        ;
955
    });
956
957
    return deferred.promise;
958
};
959
960
Wallet.prototype.decodeAddress = function(address) {
961
    return Wallet.getAddressAndType(address, this.network, this.useNewCashAddr);
962
};
963
964
function readBech32Address(address, network) {
965
    var addr;
966
    var err;
967
    try {
968
        addr = bitcoin.address.fromBech32(address, network);
969
        err = null;
970
971
    } catch (_err) {
972
        err = _err;
973
    }
974
975
    if (!err) {
976
        // Valid bech32 but invalid network immediately alerts
977
        if (addr.prefix !== network.bech32) {
978
            throw new blocktrail.InvalidAddressError("Address invalid on this network");
979
        }
980
    }
981
982
    return [err, addr];
983
}
984
985
function readCashAddress(address, network) {
986
    var addr;
987
    var err;
988
    try {
989
        addr = bitcoin.address.fromCashAddress(address);
990
        err = null;
991
    } catch (_err) {
992
        err = _err;
993
    }
994
995
    if (!err) {
996
        // Valid base58 but invalid network immediately alerts
997
        if (addr.prefix !== network.cashAddrPrefix) {
998
            throw new Error(address + ' has an invalid prefix');
999
        }
1000
    }
1001
1002
    return [err, addr];
1003
}
1004
1005
function readBase58Address(address, network) {
1006
    var addr;
1007
    var err;
1008
    try {
1009
        addr = bitcoin.address.fromBase58Check(address);
1010
        err = null;
1011
    } catch (_err) {
1012
        err = _err;
1013
    }
1014
1015
    if (!err) {
1016
        // Valid base58 but invalid network immediately alerts
1017
        if (addr.version !== network.pubKeyHash && addr.version !== network.scriptHash) {
1018
            throw new blocktrail.InvalidAddressError("Address invalid on this network");
1019
        }
1020
    }
1021
1022
    return [err, addr];
1023
}
1024
1025
Wallet.getAddressAndType = function(address, network, allowCashAddress) {
1026
    var addr;
1027
    var type;
1028
    var err;
1029
1030
    function readAddress(reader, readType) {
1031
        var decoded = reader(address, network);
1032
        if (decoded[0] === null) {
1033
            addr = decoded[1];
1034
            type = readType;
1035
        } else {
1036
            err = decoded[0];
1037
        }
1038
    }
1039
1040
    if (network === bitcoin.networks.bitcoin || network === bitcoin.networks.testnet) {
1041
        readAddress(readBech32Address, "bech32");
1042
    }
1043
1044
    if (!addr && 'cashAddrPrefix' in network && allowCashAddress) {
1045
        readAddress(readCashAddress, "cashaddr");
1046
    }
1047
1048
    if (!addr) {
1049
        readAddress(readBase58Address, "base58");
1050
    }
1051
1052
    if (addr) {
1053
        return {
1054
            address: address,
1055
            decoded: addr,
1056
            type: type
1057
        };
1058
    } else {
1059
        throw new blocktrail.InvalidAddressError(err.message);
1060
    }
1061
};
1062
1063
Wallet.convertPayToOutputs = function(pay, network, allowCashAddr) {
1064
    var send = [];
1065
1066
    var readFunc;
1067
1068
    // Deal with two different forms
1069
    if (Array.isArray(pay)) {
1070
        // output[]
1071
        readFunc = function(i, output, obj) {
1072
            if (typeof output !== "object") {
1073
                throw new Error("Invalid transaction output for numerically indexed list [1]");
1074
            }
1075
1076
            var keys = Object.keys(output);
1077
            if (keys.indexOf("scriptPubKey") !== -1 && keys.indexOf("value") !== -1) {
1078
                obj.scriptPubKey = output["scriptPubKey"];
1079
                obj.value = output["value"];
1080
            } else if (keys.indexOf("address") !== -1 && keys.indexOf("value") !== -1) {
1081
                obj.address = output["address"];
1082
                obj.value = output["value"];
1083
            } else if (keys.length === 2 && output.length === 2 && keys[0] === '0' && keys[1] === '1') {
1084
                obj.address = output[0];
1085
                obj.value = output[1];
1086
            } else {
1087
                throw new Error("Invalid transaction output for numerically indexed list [2]");
1088
            }
1089
        };
1090
    } else if (typeof pay === "object") {
1091
        // map[addr]amount
1092
        readFunc = function(address, value, obj) {
1093
            obj.address = address.trim();
1094
            obj.value = value;
1095
            if (obj.address === Wallet.OP_RETURN) {
1096
                var datachunk = Buffer.isBuffer(value) ? value : new Buffer(value, 'utf-8');
0 ignored issues
show
Bug introduced by
The variable Buffer seems to be never declared. If this is a global, consider adding a /** global: Buffer */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
1097
                obj.scriptPubKey = bitcoin.script.nullData.output.encode(datachunk).toString('hex');
1098
                obj.value = 0;
1099
                obj.address = null;
1100
            }
1101
        };
1102
    } else {
1103
        throw new Error("Invalid input");
1104
    }
1105
1106
    Object.keys(pay).forEach(function(key) {
1107
        var obj = {};
1108
        readFunc(key, pay[key], obj);
1109
1110
        if (parseInt(obj.value, 10).toString() !== obj.value.toString()) {
1111
            throw new blocktrail.WalletSendError("Values should be in Satoshis");
1112
        }
1113
1114
        // Remove address, replace with scriptPubKey
1115
        if (typeof obj.address === "string") {
1116
            try {
1117
                var addrAndType = Wallet.getAddressAndType(obj.address, network, allowCashAddr);
1118
                obj.scriptPubKey = bitcoin.address.toOutputScript(addrAndType.address, network, allowCashAddr).toString('hex');
1119
                delete obj.address;
1120
            } catch (e) {
1121
                throw new blocktrail.InvalidAddressError("Invalid address [" + obj.address + "] (" + e.message + ")");
1122
            }
1123
        }
1124
1125
        // Extra checks when the output isn't OP_RETURN
1126
        if (obj.scriptPubKey.slice(0, 2) !== "6a") {
1127
            if (!(obj.value = parseInt(obj.value, 10))) {
1128
                throw new blocktrail.WalletSendError("Values should be non zero");
1129
            } else if (obj.value <= blocktrail.DUST) {
1130
                throw new blocktrail.WalletSendError("Values should be more than dust (" + blocktrail.DUST + ")");
1131
            }
1132
        }
1133
1134
        // Value fully checked now
1135
        obj.value = parseInt(obj.value, 10);
1136
1137
        send.push(obj);
1138
    });
1139
1140
    return send;
1141
};
1142
1143
Wallet.prototype.buildTransaction = function(pay, changeAddress, allowZeroConf, randomizeChangeIdx, feeStrategy, options, cb) {
1144
    /* jshint -W071 */
1145
    var self = this;
1146
1147
    if (typeof changeAddress === "function") {
1148
        cb = changeAddress;
1149
        changeAddress = null;
1150
    } else if (typeof allowZeroConf === "function") {
1151
        cb = allowZeroConf;
1152
        allowZeroConf = false;
1153
    } else if (typeof randomizeChangeIdx === "function") {
1154
        cb = randomizeChangeIdx;
1155
        randomizeChangeIdx = true;
1156
    } else if (typeof feeStrategy === "function") {
1157
        cb = feeStrategy;
1158
        feeStrategy = null;
1159
    } else if (typeof options === "function") {
1160
        cb = options;
1161
        options = {};
1162
    }
1163
1164
    randomizeChangeIdx = typeof randomizeChangeIdx !== "undefined" ? randomizeChangeIdx : true;
1165
    feeStrategy = feeStrategy || Wallet.FEE_STRATEGY_OPTIMAL;
1166
    options = options || {};
1167
1168
    var deferred = q.defer();
1169
    deferred.promise.spreadNodeify(cb);
1170
1171
    q.nextTick(function() {
1172
        var send;
1173
        try {
1174
            send = Wallet.convertPayToOutputs(pay, self.network, self.useNewCashAddr);
1175
        } catch (e) {
1176
            deferred.reject(e);
1177
            return deferred.promise;
1178
        }
1179
1180
        if (!send.length) {
1181
            deferred.reject(new blocktrail.WalletSendError("Need at least one recipient"));
1182
            return deferred.promise;
1183
        }
1184
1185
        deferred.notify(Wallet.PAY_PROGRESS_COIN_SELECTION);
1186
1187
        deferred.resolve(
1188
            self.coinSelection(send, true, allowZeroConf, feeStrategy, options)
1189
            /**
1190
             *
1191
             * @param {Object[]} utxos
1192
             * @param fee
1193
             * @param change
1194
             * @param randomizeChangeIdx
0 ignored issues
show
Documentation introduced by
The parameter randomizeChangeIdx does not exist. Did you maybe forget to remove this comment?
Loading history...
1195
             * @returns {*}
1196
             */
1197
                .spread(function(utxos, fee, change) {
1198
                    var tx, txb, outputs = [];
1199
1200
                    var deferred = q.defer();
1201
1202
                    async.waterfall([
1203
                        /**
1204
                         * prepare
1205
                         *
1206
                         * @param cb
1207
                         */
1208
                        function(cb) {
1209
                            var inputsTotal = utxos.map(function(utxo) {
1210
                                return utxo['value'];
1211
                            }).reduce(function(a, b) {
1212
                                return a + b;
1213
                            });
1214
                            var outputsTotal = send.map(function(output) {
1215
                                return output.value;
1216
                            }).reduce(function(a, b) {
1217
                                return a + b;
1218
                            });
1219
                            var estimatedChange = inputsTotal - outputsTotal - fee;
1220
1221
                            if (estimatedChange > blocktrail.DUST * 2 && estimatedChange !== change) {
1222
                                return cb(new blocktrail.WalletFeeError("the amount of change (" + change + ") " +
1223
                                    "suggested by the coin selection seems incorrect (" + estimatedChange + ")"));
1224
                            }
1225
1226
                            cb();
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
1227
                        },
1228
                        /**
1229
                         * init transaction builder
1230
                         *
1231
                         * @param cb
1232
                         */
1233
                        function(cb) {
1234
                            txb = new bitcoin.TransactionBuilder(self.network);
1235
                            if (self.bitcoinCash) {
1236
                                txb.enableBitcoinCash();
1237
                            }
1238
1239
                            cb();
1240
                        },
1241
                        /**
1242
                         * add UTXOs as inputs
1243
                         *
1244
                         * @param cb
1245
                         */
1246
                        function(cb) {
1247
                            var i;
1248
1249
                            for (i = 0; i < utxos.length; i++) {
1250
                                txb.addInput(utxos[i]['hash'], utxos[i]['idx']);
1251
                            }
1252
1253
                            cb();
1254
                        },
1255
                        /**
1256
                         * build desired outputs
1257
                         *
1258
                         * @param cb
1259
                         */
1260
                        function(cb) {
1261
                            send.forEach(function(_send) {
1262
                                if (_send.scriptPubKey) {
1263
                                    outputs.push({scriptPubKey: new Buffer(_send.scriptPubKey, 'hex'), value: _send.value});
0 ignored issues
show
Bug introduced by
The variable Buffer seems to be never declared. If this is a global, consider adding a /** global: Buffer */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
1264
                                } else {
1265
                                    throw new Error("Invalid send");
1266
                                }
1267
                            });
1268
                            cb();
1269
                        },
1270
                        /**
1271
                         * get change address if required
1272
                         *
1273
                         * @param cb
1274
                         */
1275
                        function(cb) {
1276
                            if (change > 0) {
1277
                                if (change <= blocktrail.DUST) {
1278
                                    change = 0; // don't do a change output if it would be a dust output
1279
1280
                                } else {
1281
                                    if (!changeAddress) {
1282
                                        deferred.notify(Wallet.PAY_PROGRESS_CHANGE_ADDRESS);
1283
1284
                                        return self.getNewAddress(self.changeChain, function(err, address) {
1285
                                            if (err) {
1286
                                                return cb(err);
1287
                                            }
1288
                                            changeAddress = address;
1289
                                            cb();
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
1290
                                        });
1291
                                    }
1292
                                }
1293
                            }
1294
1295
                            cb();
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
1296
                        },
1297
                        /**
1298
                         * add change to outputs
1299
                         *
1300
                         * @param cb
1301
                         */
1302
                        function(cb) {
1303
                            if (change > 0) {
1304
                                var changeOutput = {
1305
                                    scriptPubKey: bitcoin.address.toOutputScript(changeAddress, self.network, self.useNewCashAddr),
1306
                                    value: change
1307
                                };
1308
                                if (randomizeChangeIdx) {
1309
                                    outputs.splice(_.random(0, outputs.length), 0, changeOutput);
1310
                                } else {
1311
                                    outputs.push(changeOutput);
1312
                                }
1313
                            }
1314
1315
                            cb();
1316
                        },
1317
                        /**
1318
                         * add outputs to txb
1319
                         *
1320
                         * @param cb
1321
                         */
1322
                        function(cb) {
1323
                            outputs.forEach(function(outputInfo) {
1324
                                txb.addOutput(outputInfo.scriptPubKey, outputInfo.value);
1325
                            });
1326
1327
                            cb();
1328
                        },
1329
                        /**
1330
                         * sign
1331
                         *
1332
                         * @param cb
1333
                         */
1334
                        function(cb) {
1335
                            var i, privKey, path, redeemScript, witnessScript;
1336
1337
                            deferred.notify(Wallet.PAY_PROGRESS_SIGN);
1338
1339
                            for (i = 0; i < utxos.length; i++) {
1340
                                var mode = SignMode.SIGN;
1341
                                if (utxos[i].sign_mode) {
1342
                                    mode = utxos[i].sign_mode;
1343
                                }
1344
1345
                                redeemScript = null;
0 ignored issues
show
Unused Code introduced by
The assignment to redeemScript seems to be never used. If you intend to free memory here, this is not necessary since the variable leaves the scope anyway.
Loading history...
1346
                                witnessScript = null;
1347
                                if (mode === SignMode.SIGN) {
1348
                                    path = utxos[i]['path'].replace("M", "m");
1349
1350
                                    // todo: regenerate scripts for path and compare for utxo (paranoid mode)
1351
                                    if (self.primaryPrivateKey) {
1352
                                        privKey = Wallet.deriveByPath(self.primaryPrivateKey, path, "m").keyPair;
1353
                                    } else if (self.backupPrivateKey) {
1354
                                        privKey = Wallet.deriveByPath(self.backupPrivateKey, path.replace(/^m\/(\d+)\'/, 'm/$1'), "m").keyPair;
1355
                                    } else {
1356
                                        throw new Error("No master privateKey present");
1357
                                    }
1358
1359
                                    redeemScript = new Buffer(utxos[i]['redeem_script'], 'hex');
0 ignored issues
show
Bug introduced by
The variable Buffer seems to be never declared. If this is a global, consider adding a /** global: Buffer */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
1360
                                    if (typeof utxos[i]['witness_script'] === 'string') {
1361
                                        witnessScript = new Buffer(utxos[i]['witness_script'], 'hex');
1362
                                    }
1363
1364
                                    var sigHash = bitcoin.Transaction.SIGHASH_ALL;
1365
                                    if (self.bitcoinCash) {
1366
                                        sigHash |= bitcoin.Transaction.SIGHASH_BITCOINCASHBIP143;
1367
                                    }
1368
1369
                                    txb.sign(i, privKey, redeemScript, sigHash, utxos[i].value, witnessScript);
1370
                                }
1371
                            }
1372
1373
                            tx = txb.buildIncomplete();
1374
1375
                            cb();
1376
                        },
1377
                        /**
1378
                         * estimate fee to verify that the API is not providing us wrong data
1379
                         *
1380
                         * @param cb
1381
                         */
1382
                        function(cb) {
1383
                            var estimatedFee = Wallet.estimateVsizeFee(tx, utxos);
1384
1385
                            if (self.sdk.feeSanityCheck) {
1386
                                switch (feeStrategy) {
0 ignored issues
show
Coding Style introduced by
As per coding-style, switch statements should have a default case.
Loading history...
1387
                                    case Wallet.FEE_STRATEGY_BASE_FEE:
0 ignored issues
show
Bug introduced by
The variable Wallet seems to be never declared. If this is a global, consider adding a /** global: Wallet */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
1388
                                        if (Math.abs(estimatedFee - fee) > blocktrail.BASE_FEE) {
1389
                                            return cb(new blocktrail.WalletFeeError("the fee suggested by the coin selection (" + fee + ") " +
1390
                                                "seems incorrect (" + estimatedFee + ") for FEE_STRATEGY_BASE_FEE"));
1391
                                        }
1392
                                    break;
1393
1394
                                    case Wallet.FEE_STRATEGY_HIGH_PRIORITY:
1395
                                    case Wallet.FEE_STRATEGY_OPTIMAL:
1396
                                    case Wallet.FEE_STRATEGY_LOW_PRIORITY:
1397
                                        if (fee > estimatedFee * self.feeSanityCheckBaseFeeMultiplier) {
1398
                                            return cb(new blocktrail.WalletFeeError("the fee suggested by the coin selection (" + fee + ") " +
1399
                                                "seems awefully high (" + estimatedFee + ") for FEE_STRATEGY_OPTIMAL"));
1400
                                        }
1401
                                    break;
1402
                                }
1403
                            }
1404
1405
                            cb();
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
1406
                        }
1407
                    ], function(err) {
1408
                        if (err) {
1409
                            deferred.reject(new blocktrail.WalletSendError(err));
1410
                            return;
1411
                        }
1412
1413
                        deferred.resolve([tx, utxos]);
1414
                    });
1415
1416
                    return deferred.promise;
1417
                }
1418
            )
1419
        );
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
1420
    });
1421
1422
    return deferred.promise;
1423
};
1424
1425
1426
/**
1427
 * use the API to get the best inputs to use based on the outputs
1428
 *
1429
 * @param pay               array       {'address': (int)value}     coins to send
1430
 * @param [lockUTXO]        bool        lock UTXOs for a few seconds to allow for transaction to be created
1431
 * @param [allowZeroConf]   bool        allow zero confirmation unspent outputs to be used in coin selection
1432
 * @param [feeStrategy]     string      defaults to FEE_STRATEGY_OPTIMAL
1433
 * @param [options]         object
1434
 * @param [cb]              function    callback(err, utxos, fee, change)
1435
 * @returns {q.Promise}
1436
 */
1437
Wallet.prototype.coinSelection = function(pay, lockUTXO, allowZeroConf, feeStrategy, options, cb) {
1438
    var self = this;
1439
1440
    if (typeof lockUTXO === "function") {
1441
        cb = lockUTXO;
1442
        lockUTXO = true;
1443
    } else if (typeof allowZeroConf === "function") {
1444
        cb = allowZeroConf;
1445
        allowZeroConf = false;
1446
    } else if (typeof feeStrategy === "function") {
1447
        cb = feeStrategy;
1448
        feeStrategy = null;
1449
    } else if (typeof options === "function") {
1450
        cb = options;
1451
        options = {};
1452
    }
1453
1454
    lockUTXO = typeof lockUTXO !== "undefined" ? lockUTXO : true;
1455
    feeStrategy = feeStrategy || Wallet.FEE_STRATEGY_OPTIMAL;
1456
    options = options || {};
1457
1458
    var send;
1459
    try {
1460
        send = Wallet.convertPayToOutputs(pay, self.network, self.useNewCashAddr);
1461
    } catch (e) {
1462
        var deferred = q.defer();
1463
        deferred.promise.nodeify(cb);
1464
        deferred.reject(e);
1465
        return deferred.promise;
1466
    }
1467
1468
    return self.sdk.coinSelection(self.identifier, send, lockUTXO, allowZeroConf, feeStrategy, options, cb);
1469
};
1470
1471
/**
1472
 * send the transaction using the API
1473
 *
1474
 * @param txHex             string      partially signed transaction as hex string
1475
 * @param paths             array       list of paths used in inputs which should be cosigned by the API
1476
 * @param checkFee          bool        when TRUE the API will verify if the fee is 100% correct and otherwise throw an exception
1477
 * @param [twoFactorToken]  string      2FA token
1478
 * @param prioboost         bool
1479
 * @param [cb]              function    callback(err, txHash)
1480
 * @returns {q.Promise}
1481
 */
1482
Wallet.prototype.sendTransaction = function(txHex, paths, checkFee, twoFactorToken, prioboost, cb) {
1483
    var self = this;
1484
1485
    if (typeof twoFactorToken === "function") {
1486
        cb = twoFactorToken;
1487
        twoFactorToken = null;
1488
        prioboost = false;
1489
    } else if (typeof prioboost === "function") {
1490
        cb = twoFactorToken;
1491
        prioboost = false;
1492
    }
1493
1494
    var deferred = q.defer();
1495
    deferred.promise.nodeify(cb);
1496
1497
    self.sdk.sendTransaction(self.identifier, txHex, paths, checkFee, twoFactorToken, prioboost)
1498
        .then(
1499
            function(result) {
1500
                deferred.resolve(result);
1501
            },
1502
            function(e) {
1503
                if (e.requires_2fa) {
1504
                    deferred.reject(new blocktrail.WalletMissing2FAError());
1505
                } else if (e.message.match(/Invalid two_factor_token/)) {
1506
                    deferred.reject(new blocktrail.WalletInvalid2FAError());
1507
                } else {
1508
                    deferred.reject(e);
1509
                }
1510
            }
1511
        )
1512
    ;
1513
1514
    return deferred.promise;
1515
};
1516
1517
/**
1518
 * setup a webhook for this wallet
1519
 *
1520
 * @param url           string      URL to receive webhook events
1521
 * @param [identifier]  string      identifier for the webhook, defaults to WALLET- + wallet.identifier
1522
 * @param [cb]          function    callback(err, webhook)
1523
 * @returns {q.Promise}
1524
 */
1525
Wallet.prototype.setupWebhook = function(url, identifier, cb) {
1526
    var self = this;
1527
1528
    if (typeof identifier === "function") {
1529
        cb = identifier;
1530
        identifier = null;
1531
    }
1532
1533
    identifier = identifier || ('WALLET-' + self.identifier);
1534
1535
    return self.sdk.setupWalletWebhook(self.identifier, identifier, url, cb);
1536
};
1537
1538
/**
1539
 * delete a webhook that was created for this wallet
1540
 *
1541
 * @param [identifier]  string      identifier for the webhook, defaults to WALLET- + wallet.identifier
1542
 * @param [cb]          function    callback(err, success)
1543
 * @returns {q.Promise}
1544
 */
1545
Wallet.prototype.deleteWebhook = function(identifier, cb) {
1546
    var self = this;
1547
1548
    if (typeof identifier === "function") {
1549
        cb = identifier;
1550
        identifier = null;
1551
    }
1552
1553
    identifier = identifier || ('WALLET-' + self.identifier);
1554
1555
    return self.sdk.deleteWalletWebhook(self.identifier, identifier, cb);
1556
};
1557
1558
/**
1559
 * get all transactions for the wallet (paginated)
1560
 *
1561
 * @param [params]  object      pagination: {page: 1, limit: 20, sort_dir: 'asc'}
1562
 * @param [cb]      function    callback(err, transactions)
1563
 * @returns {q.Promise}
1564
 */
1565
Wallet.prototype.transactions = function(params, cb) {
1566
    var self = this;
1567
1568
    return self.sdk.walletTransactions(self.identifier, params, cb);
1569
};
1570
1571
Wallet.prototype.maxSpendable = function(allowZeroConf, feeStrategy, options, cb) {
1572
    var self = this;
1573
1574
    if (typeof allowZeroConf === "function") {
1575
        cb = allowZeroConf;
1576
        allowZeroConf = false;
1577
    } else if (typeof feeStrategy === "function") {
1578
        cb = feeStrategy;
1579
        feeStrategy = null;
1580
    } else if (typeof options === "function") {
1581
        cb = options;
1582
        options = {};
1583
    }
1584
1585
    if (typeof allowZeroConf === "object") {
1586
        options = allowZeroConf;
1587
        allowZeroConf = false;
1588
    } else if (typeof feeStrategy === "object") {
1589
        options = feeStrategy;
1590
        feeStrategy = null;
1591
    }
1592
1593
    options = options || {};
1594
1595
    if (typeof options.allowZeroConf !== "undefined") {
1596
        allowZeroConf = options.allowZeroConf;
1597
    }
1598
    if (typeof options.feeStrategy !== "undefined") {
1599
        feeStrategy = options.feeStrategy;
1600
    }
1601
1602
    feeStrategy = feeStrategy || Wallet.FEE_STRATEGY_OPTIMAL;
1603
1604
    return self.sdk.walletMaxSpendable(self.identifier, allowZeroConf, feeStrategy, options, cb);
1605
};
1606
1607
/**
1608
 * get all addresses for the wallet (paginated)
1609
 *
1610
 * @param [params]  object      pagination: {page: 1, limit: 20, sort_dir: 'asc'}
1611
 * @param [cb]      function    callback(err, addresses)
1612
 * @returns {q.Promise}
1613
 */
1614
Wallet.prototype.addresses = function(params, cb) {
1615
    var self = this;
1616
1617
    return self.sdk.walletAddresses(self.identifier, params, cb);
1618
};
1619
1620
/**
1621
 * @param address   string      the address to label
1622
 * @param label     string      the label
1623
 * @param [cb]      function    callback(err, res)
1624
 * @returns {q.Promise}
1625
 */
1626
Wallet.prototype.labelAddress = function(address, label, cb) {
1627
    var self = this;
1628
1629
    return self.sdk.labelWalletAddress(self.identifier, address, label, cb);
1630
};
1631
1632
/**
1633
 * get all UTXOs for the wallet (paginated)
1634
 *
1635
 * @param [params]  object      pagination: {page: 1, limit: 20, sort_dir: 'asc'}
1636
 * @param [cb]      function    callback(err, addresses)
1637
 * @returns {q.Promise}
1638
 */
1639
Wallet.prototype.utxos = function(params, cb) {
1640
    var self = this;
1641
1642
    return self.sdk.walletUTXOs(self.identifier, params, cb);
1643
};
1644
1645
Wallet.prototype.unspentOutputs = Wallet.prototype.utxos;
1646
1647
/**
1648
 * sort list of pubkeys to be used in a multisig redeemscript
1649
 *  sorted in lexicographical order on the hex of the pubkey
1650
 *
1651
 * @param pubKeys   {bitcoin.HDNode[]}
1652
 * @returns string[]
1653
 */
1654
Wallet.sortMultiSigKeys = function(pubKeys) {
1655
    pubKeys.sort(function(key1, key2) {
1656
        return key1.toString('hex').localeCompare(key2.toString('hex'));
1657
    });
1658
1659
    return pubKeys;
1660
};
1661
1662
/**
1663
 * determine how much fee is required based on the inputs and outputs
1664
 *  this is an estimation, not a proper 100% correct calculation
1665
 *
1666
 * @todo: mark deprecated in favor of estimations where UTXOS are known
1667
 * @param {bitcoin.Transaction} tx
1668
 * @param {int} feePerKb when not null use this feePerKb, otherwise use BASE_FEE legacy calculation
1669
 * @returns {number}
1670
 */
1671
Wallet.estimateIncompleteTxFee = function(tx, feePerKb) {
1672
    var size = Wallet.estimateIncompleteTxSize(tx);
1673
    var sizeKB = size / 1000;
1674
    var sizeKBCeil = Math.ceil(size / 1000);
1675
1676
    if (feePerKb) {
1677
        return parseInt(sizeKB * feePerKb, 10);
1678
    } else {
1679
        return parseInt(sizeKBCeil * blocktrail.BASE_FEE, 10);
1680
    }
1681
};
1682
1683
/**
1684
 * Takes tx and utxos, computing their estimated vsize,
1685
 * and uses feePerKb (or BASEFEE as default) to estimate
1686
 * the number of satoshis in fee.
1687
 *
1688
 * @param {bitcoin.Transaction} tx
1689
 * @param {Array} utxos
1690
 * @param feePerKb
1691
 * @returns {Number}
1692
 */
1693
Wallet.estimateVsizeFee = function(tx, utxos, feePerKb) {
1694
    var vsize = SizeEstimation.estimateTxVsize(tx, utxos);
1695
    var sizeKB = vsize / 1000;
1696
    var sizeKBCeil = Math.ceil(vsize / 1000);
1697
1698
    if (feePerKb) {
1699
        return parseInt(sizeKB * feePerKb, 10);
1700
    } else {
1701
        return parseInt(sizeKBCeil * blocktrail.BASE_FEE, 10);
1702
    }
1703
};
1704
1705
/**
1706
 * determine how much fee is required based on the inputs and outputs
1707
 *  this is an estimation, not a proper 100% correct calculation
1708
 *
1709
 * @param {bitcoin.Transaction} tx
1710
 * @returns {number}
1711
 */
1712
Wallet.estimateIncompleteTxSize = function(tx) {
1713
    var size = 4 + 4 + 4 + 4; // version + txinVarInt + txoutVarInt + locktime
1714
1715
    size += tx.outs.length * 34;
1716
1717
    tx.ins.forEach(function(txin) {
1718
        var scriptSig = txin.script,
1719
            scriptType = bitcoin.script.classifyInput(scriptSig);
1720
1721
        var multiSig = [2, 3]; // tmp hardcoded
1722
1723
        // Re-classify if P2SH
1724
        if (!multiSig && scriptType === 'scripthash') {
1725
            var sigChunks = bitcoin.script.decompile(scriptSig);
1726
            var redeemScript = sigChunks.slice(-1)[0];
1727
            scriptSig = bitcoin.script.compile(sigChunks.slice(0, -1));
1728
            scriptType = bitcoin.script.classifyInput(scriptSig);
1729
1730
            if (bitcoin.script.classifyOutput(redeemScript) !== scriptType) {
1731
                throw new blocktrail.TransactionInputError('Non-matching scriptSig and scriptPubKey in input');
1732
            }
1733
1734
            // figure out M of N for multisig (code from internal usage of bitcoinjs)
1735
            if (scriptType === 'multisig') {
1736
                var rsChunks = bitcoin.script.decompile(redeemScript);
1737
                var mOp = rsChunks[0];
1738
                if (mOp === bitcoin.opcodes.OP_0 || mOp < bitcoin.opcodes.OP_1 || mOp > bitcoin.opcodes.OP_16) {
1739
                    throw new blocktrail.TransactionInputError("Invalid multisig redeemScript");
1740
                }
1741
1742
                var nOp = rsChunks[redeemScript.chunks.length - 2];
1743
                if (mOp === bitcoin.opcodes.OP_0 || mOp < bitcoin.opcodes.OP_1 || mOp > bitcoin.opcodes.OP_16) {
1744
                    throw new blocktrail.TransactionInputError("Invalid multisig redeemScript");
1745
                }
1746
1747
                var m = mOp - (bitcoin.opcodes.OP_1 - 1);
1748
                var n = nOp - (bitcoin.opcodes.OP_1 - 1);
1749
                if (n < m) {
1750
                    throw new blocktrail.TransactionInputError("Invalid multisig redeemScript");
1751
                }
1752
1753
                multiSig = [m, n];
1754
            }
1755
        }
1756
1757
        if (multiSig) {
1758
            size += (
1759
                32 + // txhash
1760
                4 + // idx
1761
                3 + // scriptVarInt[>=253]
1762
                1 + // OP_0
1763
                ((1 + 72) * multiSig[0]) + // (OP_PUSHDATA[<75] + 72) * sigCnt
1764
                (2 + 105) + // OP_PUSHDATA[>=75] + script
1765
                4 // sequence
1766
            );
1767
1768
        } else {
1769
            size += 32 + // txhash
1770
                4 + // idx
1771
                73 + // sig
1772
                34 + // script
1773
                4; // sequence
1774
        }
1775
    });
1776
1777
    return size;
1778
};
1779
1780
/**
1781
 * determine how much fee is required based on the amount of inputs and outputs
1782
 *  this is an estimation, not a proper 100% correct calculation
1783
 *  this asumes all inputs are 2of3 multisig
1784
 *
1785
 * @todo: mark deprecated in favor of situations where UTXOS are known
1786
 * @param txinCnt       {number}
1787
 * @param txoutCnt      {number}
1788
 * @returns {number}
1789
 */
1790
Wallet.estimateFee = function(txinCnt, txoutCnt) {
1791
    var size = 4 + 4 + 4 + 4; // version + txinVarInt + txoutVarInt + locktime
1792
1793
    size += txoutCnt * 34;
1794
1795
    size += (
1796
            32 + // txhash
1797
            4 + // idx
1798
            3 + // scriptVarInt[>=253]
1799
            1 + // OP_0
1800
            ((1 + 72) * 2) + // (OP_PUSHDATA[<75] + 72) * sigCnt
1801
            (2 + 105) + // OP_PUSHDATA[>=75] + script
1802
            4 // sequence
1803
        ) * txinCnt;
1804
1805
    var sizeKB = Math.ceil(size / 1000);
1806
1807
    return sizeKB * blocktrail.BASE_FEE;
1808
};
1809
1810
/**
1811
 * create derived key from parent key by path
1812
 *
1813
 * @param hdKey     {bitcoin.HDNode}
1814
 * @param path      string
1815
 * @param keyPath   string
1816
 * @returns {bitcoin.HDNode}
1817
 */
1818
Wallet.deriveByPath = function(hdKey, path, keyPath) {
1819
    keyPath = keyPath || (!!hdKey.keyPair.d ? "m" : "M");
1820
1821
    if (path[0].toLowerCase() !== "m" || keyPath[0].toLowerCase() !== "m") {
1822
        throw new blocktrail.KeyPathError("Wallet.deriveByPath only works with absolute paths. (" + path + ", " + keyPath + ")");
1823
    }
1824
1825
    if (path[0] === "m" && keyPath[0] === "M") {
1826
        throw new blocktrail.KeyPathError("Wallet.deriveByPath can't derive private path from public parent. (" + path + ", " + keyPath + ")");
1827
    }
1828
1829
    // if the desired path is public while the input is private
1830
    var toPublic = path[0] === "M" && keyPath[0] === "m";
1831
    if (toPublic) {
1832
        // derive the private path, convert to public when returning
1833
        path[0] = "m";
1834
    }
1835
1836
    // keyPath should be the parent parent of path
1837
    if (path.toLowerCase().indexOf(keyPath.toLowerCase()) !== 0) {
1838
        throw new blocktrail.KeyPathError("Wallet.derivePath requires path (" + path + ") to be a child of keyPath (" + keyPath + ")");
1839
    }
1840
1841
    // remove the part of the path we already have
1842
    path = path.substr(keyPath.length);
1843
1844
    // iterate over the chunks and derive
1845
    var newKey = hdKey;
1846
    path.replace(/^\//, "").split("/").forEach(function(chunk) {
1847
        if (!chunk) {
1848
            return;
1849
        }
1850
1851
        if (chunk.indexOf("'") !== -1) {
1852
            chunk = parseInt(chunk.replace("'", ""), 10) + bitcoin.HDNode.HIGHEST_BIT;
1853
        }
1854
1855
        newKey = newKey.derive(parseInt(chunk, 10));
1856
    });
1857
1858
    if (toPublic) {
1859
        return newKey.neutered();
1860
    } else {
1861
        return newKey;
1862
    }
1863
};
1864
1865
module.exports = Wallet;
1866